递归神经网络
这里的RNN指的是时间递归层面的神经网络。它的出现是为了解决传统CNN、AE、RBM等网络在序列预测(sequence prediction)问题中表现不佳的状况。CNN善于从图片、视频中提取多层次的抽象特征,AE和RBM善于发现输入数据中隐藏的结构,但是它们的网络结构决定了其不具备信息记忆的能力,而在序列预测问题中,某一位置的信息往往与之前位置的信息有关,因此需要一个带信息存储功能的网络结构,RNN便是具备信息存储能力的网络结构,如下图所示[2]:

注意这里的递归是时间上的递归,也就是说保持网络结构不变,当前时间步(time step)下的输入前向传播得到的输出信息会参与到后一时间步的计算,网络在$t$时刻的输出代表网络对前$t$个时间步的信息记忆,当它和当前时间步的输入$x_{t+1}$一同参与$t+1$时刻的前向传播时,代表了信息记忆的迭代和更新。可以看到,RNN的网络结构理论上是为了记忆前面所有的输入信息,当我们在$t+1$时刻进行预测输入时,前面$t$个时间步的信息参与了预测的过程。但是实际上RNN在长期信息的记忆能力很差,原因是梯度的消失和爆炸问题。
LSTM
为了解决RNN在长期记忆能力上的短板,从网络结构上进行改进,得到了LSTM模型,RNN和LSTM在结构上的差异如下图[2]所示:


LSTM由基本的记忆单元(memory cell)构成,记忆单元又由四部分组成:单元状态(cell state)、遗忘控制层(forget gate layer)、输入控制层(input gate layer)、输出控制层(output gate layer)。
valilla LSTM
这是最原始的LSTM网络结构:一个包含多个memory cell的LSTM层,接上一个全连阶层产生输出。我们通过求解一个简单序列预测问题来熟悉基本LSTM的用法。
问题
给定一个随机的固定长度的整数输入序列$[ x_1, x_2, \ldots, x_T ]$,我们希望LSTM通过学习能够记住输入序列中固定位置的值$x_i$。比如输入[3,5,7,9],我们训练的LSTM是希望它能够输出第三个位置的元素值,则我们期待的输出结果为7.
思路
上述问题本质上是一个多分类问题,我们期待的结果是输入序列中特定位置的某个值,因此考虑用one-hot编码对输入数据进行处理,输出的也是one-hot编码值。我们采用如下图所示的LSTM网络结构:一个LSTM层接上一个全连接层。
输入序列中的元素代表不同时间步(time step)的输入,输入数据的特征数等于one-hot编码值的长度。全连接层对于每个输入序列只输出一个时间步的向量,这个向量就是近似one-hot值。
步骤
1、准备数据
首先需要随机生成多个固定长度的输入序列,这可以通过randint()函数完成:
其中的length参数决定输入序列的长度,n_features参数决定序列中元素的取值范围$[0,\text{n_features} )$
然后需要对输入序列中的所有元素值进行one-hot编码,编码后的one-hot长度为n_features值,元素值正好对应one-hot中“1”的索引值:
我们还需要一个对one-hot编码值进行解码的函数,用于对全连接层的输出向量进行解码操作:
因为全连接层输出的向量是softmax输出的所有可能结果的概率分布,因此解码时不是寻找“1”所在的索引,而是寻找最大值所在的索引。
输入序列的生成和编解码基本完成。不过因为我们用Keras来搭建LSTM模型,而Keras中LSTM层的输入要求是三维的数据:(batch_sizes, time steps, features),分别表示输入的样本数量、每个样本包含的时间步数、每个时间步所包含的特征数,因此需要对输入序列进行维度的转换:
其中时间步参数由序列的长度length指定,特征数由每个元素的one-hot编码长度n_features决定,参数out_index表示我们希望LSTM能够预测的元素位置。最后,通过一个统一的函数来产生直接可用的输入序列数据:
2、搭建和编译网络
我们只需要两个网络层:一个包含多个memory cell的LSTM层和全连接层,它们分别通过Keras中的LSTM()和Dense()实现:
我们建立了一个包含25个memory cell的LSTM层,它包括length个时间步的输入,每个输入包含n_features个特征,再此基础上,增加一个包含n_features个神经元的全连接层,输出值通过softmax转换为概率分布。搭好基本网络结构之后,还需要对网络进行编译后才能用于训练和测试:
编译时指定损失函数为适用于多分类问题的交叉熵损失(这里如果是二分类问题的话,则应该指定’binary_crossentropy’为损失函数),优化方法选择了Adam算法,指定分类准确率作为模型的衡量标准。通过model.summary()输出查看网络的结构,检查和确认是否符合我们的要求。
3、网络的训练和评估
根据model.summary()输出的网络结构信息,如果确认网络结构符合需求,则可以进行网络的训练了。
我们循环训练模型2000次,每次的训练样本为1,需要注意的是:因为输入序列的顺序信息很重要,因此fit()函数中的参数shuffle不能设置为True。训练好之后,便可以进行模型评估:
这是模型的训练、评估和测试结果,可以看到该模型成功的学习到了如何去正确的预测:
Stacked LSTM
Stacked LSTM也就是深层LSTM,结构上就是将多个LSTM层连接起来:前一LSTM层的输出输入到连接的后一LSTM层上,根据需要可以将多个LSTM层stack在一起。为什么需要将多个LSTM层连接在一起呢?Alex Graves在它的论文[1]中如是说:
RNNs are inherently deep in time, since their hidden state is a function of all previous hidden states. The question that inspired this paper was whether RNNs could also benefit from depth in space; that is from stacking multiple recurrent hidden layers on top of each other, just as feedforward layers are stacked in conventional deep networks.
因此Stacked LSTM是希望能够发现输入序列中更多更深入的隐含信息,我们可以通过一个回归类预测问题来了解它。
问题
给定一组具特定模式的输入序列,预测后续的多个元素值。这里我们对阻尼正弦函数进行采样,输入前length个元素值,预测紧临的后续output个元素值,阻尼正弦函数的形式为:
$$
f(i | period, decay) = a + b \cdot sin( {2 \pi i \over period} ) \cdot e^{-i \cdot decay}
$$
其中a,b表示特定的常数,需要保持不变,参数period表示正弦函数的振荡周期,参数decay表示曲线的衰减系数,函数对应的曲线示意图如下:
我们希望Stacked LSTM能够学习到输入序列隐藏的振荡信息和衰减信息,因此每个输入序列的period值和decay均是随机产生,常数值a和b固定不变(我们不希望模型学习到a和b的变化信息)。
思路
首先是等间距的从随机选择的阻尼正弦曲线中有序采样总共 length + output 个元素值,前length个元素值作为模型的输入序列,后output个元素值作为模型待学习的输出序列。将产生的一组训练数据输入到Stacked LSTM模型中训练。
步骤
1、准备数据
因为这里的问题是一个回归问题,因此从阻尼正弦曲线上采样得到的序列不需要进行one-hot编码,但是作为LSTM层的输入,同样需要进行维度转换:
2、搭建和编译网络
我们stack两个LSTM层,再加上一个由output个神经元组成的全连接层,结构示意图如下:

因为默认的LSTM对于多时间步的输入序列只产生一组输出(参见valilla LSTM的网络结构),而我们的网络结构前两层均为LSTM层,因此第二个LSTM层的输入同样必须为带时间步的输入序列,因此要求第一个LSTM层对于其每个时间步同时产生一组输出。为此,需要设置LSTM()函数中的参数return_sequences=True,代码如下:
第一,因为在一层指定了input_shape,因此后面的网络层不再需要指定输入数据的格式;第二,因为是回归问题,因此最后的全连接层采取默认的”激活函数”:线性函数$f(x) = x$。模型的损失函数选择了平均绝对误差$\text{Average} ( |y - \hat y|) $,同样采取Adam优化方法。
3、训练和测试网络
模型训练时只迭代训练一次(epochs=1),每十个输入序列进行一次网络梯度更新(batch_size = 10). 下图是模型的评估结果和测试结果,从整体MAE看,模型的拟合结果误差很小,从右图来看,预测结果的绝对值误差在$10^{-3}$级别:

CNN-LSTM
CNN的长处在于抽象特征的自动抽取,类似于一个编码器,面向的是空间型数据,比如图片。LSTM的长处在于发现序列中的隐藏信息,面向的是时间序列型数据,比如文本序列、音频序列。当我们处理的数据既包含空间型数据也包含时间型数据时,比如视频数据:它的每一帧是图片,整体是由多幅图片在时间轴上排列而成,则可以考虑是CNN-LSTM来处理,下面通过一个简单的问题来了解CNN-LSTM。
问题
给定类似如下的一组图片,预测图片中黑色方块的移动方向(下图中为向右):

思路
这是一个序列的二分类问题:将每一幅图片看作是对应时间步的输入,最后输出其中黑色方块的移动方向。因此需要用LSTM模型来处理输入数据,又因为原始输入数据是图片,其中关键的信息是黑色方块的位置信息,因此考虑用CNN来提取这一特征,作为图片的编码器,最后隐藏层的输出作为图片的特征信息输入到LSTM对应时间步上。
步骤
1、数据准备
数据准备时最重要的是输入数据的格式必须匹配:Keras中Conv2D()层要求输入数据的格式为(samples, rows, cols, channels),因为我们每一个样本有多个图片帧,需要为每个样本增加一个帧数维,因此对应的输入格式应该是(samples, frame_num, height, width, channels)。
2、网络搭建
CNN:一个卷积层 + 一个池化层;
卷积层设置2个卷积核,每个卷积核的尺寸为(2,2),步长取默认的(1,1),卷积后的结果通过RELU激活函数。卷积层输出的2个特征图输入到最大池化层,池化的尺寸和步长均为(2,2),最后通过Flatten层将池化层的单条输出数据拉成一维格式,方便输入到后面的LSTM层memory cell中。
LSTM:一个LSTM层 + 一个全连接层
LSTM层设置70个memory cell单元,因为是二分类问题,因此全连接层只需要设置一个sigmoid神经元。
连接
搭建好了CNN和LSTM后,最后还需要将CNN的输出输入到LSTM层各个时间步的memory cell上。不能够直接连接 CNN的输出层到LSTM层,因为LSTM层需要的输入数据格式是(batch_sizes, timesteps, input_dimensions),它需要的是带时间步的输入序列,而CNN对于给定的一个图片帧输出相应的特征向量,CNN看不到输入图片帧序列中的”时间步”信息,输出的特征向量之间也不带有”时间步”信息。我们的目标是希望将CNN模型重复的作用到一个样本的多个图片帧上,产生带时间步维度的输出结果,keras中的TimeDistributed Wrapper正是为了解决这个问题:只需要将cnn放进TimeDistributed Wrapper中,对应的网络结构示意图如下:

假设CNN输出的特征向量维度为(cnn_out_dim),不加TimeDistributed Wrapper时,对于输入CNN的一个样本(frame_num, height, width, channels),CNN会输出 frame_num 个维度为(cnn_out_dim)的特征;加上 TimeDistributed Wrapper 后,对于输入CNN的一个样本(frame_num, height, width, channels),输出的特征维度为(frame_num, cnn_out_dim),这正好符合LSTM层中 input_shape 的格式。
3、模型训练、评估
下面是输出结果,可以看到,在训练数据上的分类准确率接近94%,在随机生成的测试数据上分类率100%:
